package com.kaver.feign.rpc.support;

import static feign.Util.checkState;
import static feign.Util.emptyToNull;
import static org.springframework.core.annotation.AnnotatedElementUtils.findMergedAnnotation;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;

import feign.Contract;
import feign.MethodMetadata;
import feign.Util;

public class SpringMvcContract extends Contract.BaseContract {

	private static final String ACCEPT = "Accept";

	private static final String CONTENT_TYPE = "Content-Type";

	private static final String CLIENT_TYPE = "Client-Type";

	private static Set<String> MAPPING_SET = new HashSet<>();

	static {
		MAPPING_SET.add(RequestMapping.class.getName());
		MAPPING_SET.add(PostMapping.class.getName());
		MAPPING_SET.add(GetMapping.class.getName());
		MAPPING_SET.add(PutMapping.class.getName());
		MAPPING_SET.add(DeleteMapping.class.getName());
		MAPPING_SET.add(PatchMapping.class.getName());
	}

	public static boolean contantRequestMapping(Method method) {
		Annotation[] annotations = method.getAnnotations();
		if (annotations == null || annotations.length == 0) {
			return false;
		}
		for (Annotation annotation : annotations) {
			if (annotation instanceof RequestMapping) {
				return true;
			}
			if (annotation instanceof PostMapping) {
				return true;
			}
			if (annotation instanceof GetMapping) {
				return true;
			}
			if (annotation instanceof PutMapping) {
				return true;
			}
			if (annotation instanceof DeleteMapping) {
				return true;
			}
			if (annotation instanceof PatchMapping) {
				return true;
			}
		}
		return false;
	}

	@Override
	public List<MethodMetadata> parseAndValidatateMetadata(Class<?> targetType) {
		checkState(targetType.getTypeParameters().length == 0, "Parameterized types unsupported: %s",
				targetType.getSimpleName());

		Map<String, MethodMetadata> result = new LinkedHashMap<String, MethodMetadata>();
		for (Method method : targetType.getMethods()) {
			if (method.getDeclaringClass() == Object.class || (method.getModifiers() & Modifier.STATIC) != 0
					|| Util.isDefault(method) || !contantRequestMapping(method)) {
				continue;
			}
			MethodMetadata metadata = parseAndValidateMetadata(targetType, method);
			checkState(!result.containsKey(metadata.configKey()), "Overrides unsupported: %s", metadata.configKey());
			result.put(metadata.configKey(), metadata);
		}
		return new ArrayList<MethodMetadata>(result.values());
	}

	@Override
	public MethodMetadata parseAndValidateMetadata(Class<?> targetType, Method method) {
		MethodMetadata md = super.parseAndValidateMetadata(targetType, method);

		RequestMapping classAnnotation = method.getDeclaringClass().getAnnotation(RequestMapping.class);

		if (classAnnotation == null) {
			classAnnotation = targetType.getAnnotation(RequestMapping.class);
		}

		if (classAnnotation != null) {
//			String[] paths = getPath(classAnnotation);
//			// Prepend path from class annotation if specified
//			if (paths.length > 0) {
//				String pathValue = emptyToNull(paths[0]);
//				checkState(pathValue != null, "RequestMapping.value() was empty on type %s",
//						method.getDeclaringClass().getName());
//				if (!pathValue.startsWith("/")) {
//					pathValue = "/" + pathValue;
//				}
//				md.template().insert(0, pathValue);
//			}

			// produces - use from class annotation only if method has not specified this
			if (!md.template().headers().containsKey(ACCEPT)) {
				parseProduces(md, method, classAnnotation);
			}

			// consumes -- use from class annotation only if method has not specified this
			if (!md.template().headers().containsKey(CONTENT_TYPE)) {
				parseConsumes(md, method, classAnnotation);
			}

			parseParams(md, method, classAnnotation);

			// headers -- class annotation is inherited to methods, always write these if
			// present
			parseHeaders(md, method, classAnnotation);

		}
		return md;
	}

	private String[] getPath(RequestMapping methodMapping) {
		String[] paths = methodMapping.path();
		if (paths != null && paths.length > 0) {
			return paths;
		}
		return methodMapping.value();
	}

	private RequestMapping getMergedRequestMapping(Annotation methodAnnotation) {
		if (methodAnnotation instanceof RequestMapping) {
			return (RequestMapping) methodAnnotation;
		} else if (methodAnnotation instanceof PostMapping) {
			PostMapping mapping = (PostMapping) methodAnnotation;
			RequestMapping requestMapping = new RequestMapping() {
				@Override
				public Class<? extends Annotation> annotationType() {
					return mapping.annotationType();
				}

				@Override
				public String[] value() {
					return mapping.value();
				}

				@Override
				public String[] produces() {
					return mapping.produces();
				}

				@Override
				public String[] path() {
					return mapping.path();
				}

				@Override
				public String[] params() {
					return mapping.params();
				}

				@Override
				public String name() {
					return mapping.name();
				}

				@Override
				public RequestMethod[] method() {
					return new RequestMethod[] { RequestMethod.POST };
				}

				@Override
				public String[] headers() {
					return mapping.headers();
				}

				@Override
				public String[] consumes() {
					return mapping.consumes();
				}
			};
			return requestMapping;
		} else if (methodAnnotation instanceof GetMapping) {
			GetMapping mapping = (GetMapping) methodAnnotation;
			RequestMapping requestMapping = new RequestMapping() {
				@Override
				public Class<? extends Annotation> annotationType() {
					return mapping.annotationType();
				}

				@Override
				public String[] value() {
					return mapping.value();
				}

				@Override
				public String[] produces() {
					return mapping.produces();
				}

				@Override
				public String[] path() {
					return mapping.path();
				}

				@Override
				public String[] params() {
					return mapping.params();
				}

				@Override
				public String name() {
					return mapping.name();
				}

				@Override
				public RequestMethod[] method() {
					return new RequestMethod[] { RequestMethod.GET };
				}

				@Override
				public String[] headers() {
					return mapping.headers();
				}

				@Override
				public String[] consumes() {
					return mapping.consumes();
				}
			};
			return requestMapping;
		} else if (methodAnnotation instanceof PutMapping) {
			PutMapping mapping = (PutMapping) methodAnnotation;
			RequestMapping requestMapping = new RequestMapping() {
				@Override
				public Class<? extends Annotation> annotationType() {
					return mapping.annotationType();
				}

				@Override
				public String[] value() {
					return mapping.value();
				}

				@Override
				public String[] produces() {
					return mapping.produces();
				}

				@Override
				public String[] path() {
					return mapping.path();
				}

				@Override
				public String[] params() {
					return mapping.params();
				}

				@Override
				public String name() {
					return mapping.name();
				}

				@Override
				public RequestMethod[] method() {
					return new RequestMethod[] { RequestMethod.PUT };
				}

				@Override
				public String[] headers() {
					return mapping.headers();
				}

				@Override
				public String[] consumes() {
					return mapping.consumes();
				}
			};
			return requestMapping;
		} else if (methodAnnotation instanceof DeleteMapping) {
			DeleteMapping mapping = (DeleteMapping) methodAnnotation;
			RequestMapping requestMapping = new RequestMapping() {
				@Override
				public Class<? extends Annotation> annotationType() {
					return mapping.annotationType();
				}

				@Override
				public String[] value() {
					return mapping.value();
				}

				@Override
				public String[] produces() {
					return mapping.produces();
				}

				@Override
				public String[] path() {
					return mapping.path();
				}

				@Override
				public String[] params() {
					return mapping.params();
				}

				@Override
				public String name() {
					return mapping.name();
				}

				@Override
				public RequestMethod[] method() {
					return new RequestMethod[] { RequestMethod.DELETE };
				}

				@Override
				public String[] headers() {
					return mapping.headers();
				}

				@Override
				public String[] consumes() {
					return mapping.consumes();
				}
			};
			return requestMapping;
		} else if (methodAnnotation instanceof PatchMapping) {
			PatchMapping mapping = (PatchMapping) methodAnnotation;
			RequestMapping requestMapping = new RequestMapping() {
				@Override
				public Class<? extends Annotation> annotationType() {
					return mapping.annotationType();
				}

				@Override
				public String[] value() {
					return mapping.value();
				}

				@Override
				public String[] produces() {
					return mapping.produces();
				}

				@Override
				public String[] path() {
					return mapping.path();
				}

				@Override
				public String[] params() {
					return mapping.params();
				}

				@Override
				public String name() {
					return mapping.name();
				}

				@Override
				public RequestMethod[] method() {
					return new RequestMethod[] { RequestMethod.PATCH };
				}

				@Override
				public String[] headers() {
					return mapping.headers();
				}

				@Override
				public String[] consumes() {
					return mapping.consumes();
				}
			};
			return requestMapping;
		}
		return null;
	}

	@Override
	protected void processAnnotationOnMethod(MethodMetadata data, Annotation methodAnnotation, Method method) {

		RequestMapping methodMapping = getMergedRequestMapping(methodAnnotation);
		if (methodMapping == null) {
			return;
		}
		String[] paths = getPath(methodMapping);
		// HTTP Method
		checkOne(method, methodMapping.method(), "method");
		data.template().method(methodMapping.method()[0].name());

		// path
		checkAtMostOne(method, paths, "value");
		if (paths.length > 0) {
			String pathValue = emptyToNull(paths[0]);
			if (pathValue != null) {
				// Append path from @RequestMapping if value is present on method
				if (!pathValue.startsWith("/") && !data.template().toString().endsWith("/")) {
					pathValue = "/" + pathValue;
				}
				data.template().append(pathValue);
			}
		}

		// produces
		parseProduces(data, method, methodMapping);

		// consumes
		parseConsumes(data, method, methodMapping);

		// headers
		parseHeaders(data, method, methodMapping);

		// add Client-Type Header
		if (!data.template().headers().containsKey(CLIENT_TYPE)) {
			data.template().header(CLIENT_TYPE, "Feign-Client");
		}

		parseParams(data, method, methodMapping);

	}

	private void checkAtMostOne(Method method, Object[] values, String fieldName) {
		checkState(values != null && (values.length == 0 || values.length == 1),
				"Method %s can only contain at most 1 %s field. Found: %s", method.getName(), fieldName,
				values == null ? null : Arrays.asList(values));
	}

	private void checkOne(Method method, Object[] values, String fieldName) {
		checkState(values != null && values.length == 1, "Method %s can only contain 1 %s field. Found: %s",
				method.getName(), fieldName, values == null ? null : Arrays.asList(values));
	}

	@SuppressWarnings("deprecation")
	@Override
	protected boolean processAnnotationsOnParameter(MethodMetadata data, Annotation[] annotations, int paramIndex) {
		boolean isHttpAnnotation = false;
		// TODO: support spring parameter annotations?
		for (Annotation parameterAnnotation : annotations) {
			Class<? extends Annotation> annotationType = parameterAnnotation.annotationType();
			if (annotationType == PathVariable.class) {
				String name = PathVariable.class.cast(parameterAnnotation).value();
				checkState(emptyToNull(name) != null, "PathVariable annotation was empty on param %s.", paramIndex);
				nameParam(data, name, paramIndex);
				isHttpAnnotation = true;
				String varName = '{' + name + '}';
				if (data.template().url().indexOf(varName) == -1 && !searchMapValues(data.template().queries(), varName)
						&& !searchMapValues(data.template().headers(), varName)) {
					data.formParams().add(name);
				}
			} else if (annotationType == RequestParam.class) {
				String name = RequestParam.class.cast(parameterAnnotation).value();
				checkState(emptyToNull(name) != null, "QueryParam.value() was empty on parameter %s", paramIndex);

				Collection<String> query = addTemplatedParam(data.template().queries().get(name), name);
				data.template().query(name, query);
				nameParam(data, name, paramIndex);
				isHttpAnnotation = true;
			} else if (annotationType == RequestHeader.class) {
				String name = RequestHeader.class.cast(parameterAnnotation).value();
				checkState(emptyToNull(name) != null, "HeaderParam.value() was empty on parameter %s", paramIndex);
				Collection<String> header = addTemplatedParam(data.template().headers().get(name), name);
				data.template().header(name, header);
				nameParam(data, name, paramIndex);
				isHttpAnnotation = true;
			}

			// TODO
			/*
			 * else if (annotationType == FormParam.class) { String name =
			 * FormParam.class.cast(parameterAnnotation).value();
			 * checkState(emptyToNull(name) != null,
			 * "FormParam.value() was empty on parameter %s", paramIndex);
			 * data.formParams().add(name); nameParam(data, name, paramIndex);
			 * isHttpAnnotation = true; }
			 */

		}
		return isHttpAnnotation;
	}

	private <K, V> boolean searchMapValues(Map<K, Collection<V>> map, V search) {
		Collection<Collection<V>> values = map.values();
		if (values == null) {
			return false;
		}
		for (Collection<V> entry : values) {
			if (entry.contains(search)) {
				return true;
			}
		}
		return false;
	}

	private void parseProduces(MethodMetadata md, Method method, RequestMapping annotation) {
		checkAtMostOne(method, annotation.produces(), "produces");
		String[] serverProduces = annotation.produces();
		String clientAccepts = serverProduces.length == 0 ? null : emptyToNull(serverProduces[0]);
		if (clientAccepts != null) {
			md.template().header(ACCEPT, clientAccepts);
		}
	}

	private void parseConsumes(MethodMetadata md, Method method, RequestMapping annotation) {
		checkAtMostOne(method, annotation.consumes(), "consumes");
		String[] serverConsumes = annotation.consumes();
		String clientProduces = serverConsumes.length == 0 ? null : emptyToNull(serverConsumes[0]);
		if (clientProduces != null) {
			md.template().header(CONTENT_TYPE, clientProduces);
		}
	}

	private void parseParams(MethodMetadata md, Method method, RequestMapping classAnnotation) {
		String[] params = classAnnotation.params();
		for (String param : params) {
			String[] split = param.split("=");
			md.template().query(split[0], split[1]);
		}
	}

	private void parseHeaders(MethodMetadata md, Method method, RequestMapping annotation) {
		// TODO: only supports one header value per key
		if (annotation.headers() != null && annotation.headers().length > 0) {
			for (String header : annotation.headers()) {
				int colon = header.indexOf(':');
				md.template().header(header.substring(0, colon), header.substring(colon + 2));
			}
		}

	}

	@Override
	protected void processAnnotationOnClass(MethodMetadata data, Class<?> clz) {
		if (clz.getInterfaces().length == 0) {
			RequestMapping classAnnotation = findMergedAnnotation(clz, RequestMapping.class);
			if (classAnnotation != null) {
				String[] paths = getPath(classAnnotation);
				// Prepend path from class annotation if specified
				if (paths.length > 0) {
					String pathValue = emptyToNull(paths[0]);
					checkState(pathValue != null, "RequestMapping.value() was empty on type %s", clz.getName());
					if (!pathValue.startsWith("/")) {
						pathValue = "/" + pathValue;
					}
					data.template().insert(0, pathValue);
				}
			}
		}
	}
}
